learning assembly — part 1

An Introduction to 6502 Assembly and low-level programming!

It’s not that scary, I promise.

Andrew Blance
codeburst
Published in
10 min readJul 21, 2020

--

A NES (um actually, not a “real” NES, its a NES mini).
Photo by Rohan Gangopadhyay on Unsplash

Assembly language is a low-level programming language — one where you have almost total control over your computer! I got interested in learning 6502 Assembly with the goal of making a game for the NES console. The more I learned about the language though, the more I found myself interested to learn about it for its own sake. Assembly can be a rewarding language to learn, giving you many moments of satisfaction when everything you learned begins to click together. It also provides a good space to learn about how memory, processors, and other important aspects of computing work.

On the other hand, Assembly can be punishingly complex, filled with strange symbols, and dependant on arcane knowledge. This is made harder by a lack of resources that have been written aimed at an absolute beginner (that's not to say there is not a lot of amazing resources out there!). I have tried then to write something that will be readable for someone who (like me before I began reading about the subject) has never seen a low-level language, binary, hexadecimal, or other things that you can hide from when doing higher-level programming.

High and Low

Currently, the majority of the most popular programming languages (like Python or Java) are high-level [1]. High-level languages aim to use clear and simple syntax and structure, hiding large amounts of potential complexity that may come from the computer. This can result in a language is easy to read and write, which is an obvious benefit.

However, the ease of use comes with a trade-off. By abstracting away how the computer’s hardware functions you lose an amount of efficiency and precise control of the system. Why is this? Well, though a language like Python is very “human-readable” this does not mean it is easy for your computer to read as well. For your computer to find it easy to read it needs to be “translated” into 1’s and 0’s. During this “translation” the computer will try to interpret what your code means (what is 5, what is “+”, or “=”, what does the word “loop” represent, etc). It will need to make assumptions (where do you want to store this? How much precision do you want? etc) — there is no guarantee these assumptions will be correct. Then, there is also a computational cost to this translation, it will take time for your computer to do this.

You could imagine a programming language that was significantly closer to the binary numbers the computer wants to read, more low level. A language like this would be devoid of the objects and structures high-level languages give you (arrays, lists, statements such as “while” and “for” and almost everything that makes programming a language like Python “nice”) and instead have a set of capabilities entirely determined by the hardware and manufacturer. Here, if you wanted to do something you would need it entirely by yourself — if you want to save a value you need to choose precisely where it will be stored, if you want to multiply something you need to tell it exactly what multiplying means, and so on. A language like this may be limited in what you could do (in a sense..) but you would have the benefit of knowing precisely what the computer was doing.

We will begin then by investigating the 6502 assembly programming language. This is a very different language to something like Python, R, or c++. 6502 assembly is a very low-level language that works specifically for the 6502 microprocessor — a very popular processor from the 1970s. We will begin by taking a closer look at what assembly exactly is and why we have chosen to spend time learning a variant of it that is almost 50 years old. For the time being, I will not dwell on things like how binary and hexadecimal numbers work, I will come back in a later post and give more thorough definitions. Right now, I’m just trying to present a flavor of the elements of the language.

Picture of 6502 Microprocessor
CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=91538

Why do you need a language like Assembly?

We started by thinking about high-level languages and then imagining what a low-level one would look like in comparison, here we will try to go the other way. We are going to explore what is the most basic inputs a processor wants to accept, then build up a language around that.

Binary

A processor, based on its hardware, will have several Instructions it will accept. Instruction is an action the processor can perform, like “add” or “subtract”. The complete set of instructions a processor can perform is known as its instruction set. You can ask the processor to perform an instruction by passing it the appropriate binary pattern. This is effectively as low-level we can go. For example, if the 6502 is given the 8-bit pattern 10000101 it will interpret this as Store the contents of the Accumulator in a specific memory location. At the moment, don’t worry about what exactly this means, the aim is to show that, by passing binary patterns to the processor, it will perform an instruction.

We can use these instructions to construct more complicated processes than the ones available to us as individual instructions. This is a program. A program is more though than just a list of instructions. It will need to include data to work with and locations in memory of where to store and access things. Telling the processor to add something is pointless if you can’t tell it what numbers to sum and where to store the result! Here is an example program:

10100101
01100000
01100101
01100001
10000101
01100010

It’s obvious what this does, right? (It is not.) This will add 2 numbers, and save the result somewhere. To be clear, this is not written in Assembly, this is Machine Language. Eventually, all code you write, regardless of what language, will be converted to look something like this. As someone who wants to write code though there are many very obvious flaws with machine language:

  • It is very hard to read and understand, at a glance, every line looks identical.
  • It is very hard to write, how are you meant to remember each instruction?

Hexadecimal

We need a better way to represent our data, instructions and memory locations. The Binary is nice for a computer but it is not very nice for a human. Our first step will be writing everything in hexadecimal. Here is the same program as above but written in hexadecimal:

A5
60
65
61
85
62

This is a little better than the binary equivalent. It is easier to read, write, and debug. Earlier, I made the statement that the processor will only accept things in the form of an 8-bit pattern — this is still true. Before this program can be run by the 6502 it will need to be translated into binary — this is done using a hexadecimal loader. As we begin to construct a higher-level language than machine code we begin to make trade-offs. We need, for our sanity, a more human-readable programming language, but the cost of using a loader is that is must also occupy space in memory, memory you can no longer use for running your program. Writing a program in binary (or probably even hexadecimal) would not be a sensible thing to do, so using a loader is worth the cost.

Writing in hexadecimal still has lots of clear issues. Mainly, how would you know what that code does if I had not told you beforehand? How is someone meant to remember what all those hexadecimal values represent? To reduce this issue we will add another layer of abstraction to our code: mnemonics.

Instruction Mnemonics

Here, we will give our instruction set (hopefully) memorable names. Then, instead of referring to the instructions by a hexadecimal value we will use their given names. A program that uses these mnemonics is said to be written in Assembly language. If I write the same addition example as before in 6502 assembly it will look like:

LDA   $60
ADC $61
STA $62

The program is beginning to look a lot cleaner and readable. While at the moment all this might not mean much to you, I promise it is easier to remember the mnemonics than the hex symbols. Knowing that loosely LDA means load, ADC means add and STA kinda means save allows us to begin to get an idea of what the program is doing. Even if we were not told the purpose of the code, but we knew what instructions meant, we would have a good idea of what it would do.

I have been careful here to refer to the above code as being written in “6502 assembly language” and not simply “assembly language”. This is because instruction names are decided upon by the processor manufacturer and can therefore wildly differ from processor to processor. A combination of this and different processor hardware allowing for different instructions to exist means that assembly code written for one type of processor will not run on another. This is a huge difference compared to a modern high-level language. I can write a Python script on a MacBook that has an Intel i7 processor and then easily (in theory) run it on a Windows 10 machine with an i3 processor. This is not the case with assembly. 6502 assembly code will not run on a modern x86 machine.

To translate our assembly code into something the machine can understand and run we use an assembler. This will take our program written in assembly (our source program) and return something the 6502 can run (an object program). As with the case of using the hexadecimal loader there is a cost to this but it is entirely worth it so we can avoid machine language.

A slight digression — someone pointed out to me I was playing slightly fast and loose with some terminology. I thought I better try clear it up here before we go any further. To be precise, “assembly” is a verb meaning to assemble the code into something executable. The assembler is the software that will do this. To refer to the language you should say “assembly language”.

Now, let’s go back and look at the code. I can definitely admit that our “basic” example of the language is still significantly more complicated than the Python equivalent. That would look something like:

x = y + z

However, that's just not as fun as assembly language!

Why bother learning this?

Even the small assembly addition example I presented is intimidating to look at. It is (for now!) still hard for us to read, composed of symbols that we are not used to having to read. In my opinion, it is worth persevering and learning assembly. This is for a few reasons:

  • While you probably won’t use this for any practical application learning a different programming language (I think) makes you better at any other one. This is amplified when learning assembly. If you are like me and do not know a lot about computer science, learning assembly teaches you a lot about how computers work. It demystifies what is going on underneath the hood when you run your Python program. It forces you to think about things like the stack, and memory locations and more fundamental parts of the computer. These are things you can mostly get away with (thankfully!) not knowing much about if you write in high-level languages.
  • I think its fun, but maybe I’m a nerd.
  • Mainly, it will enable to solve Euler-Project problems incredibly fast, and isn’t that the main reason to learn anything?
Another image of a 6502 microprocessor!
By Christian Bassow, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=55535656

Why the 6502?

You may wonder why we are learning 6502 assembly, especially after I said it is incompatible with modern x86–64 assembly. Here are my reasons:

  • 6502 assembly has fewer instructions than modern assembly. This, in my opinion, is a really good reason to learn 6502. 6502 assembly has around 50 instructions while you can argue (it’s complicated..) that x86–64 has around 3000 [2]. This means that will be able to realistically understand the entirety of the 6502 instruction set and get into how it works. With only around 50 instructions as well I find it less intimidating than the idea of learning modern assembly. Even if you do want to learn x86–64 I would argue this is a good place to start. The basic idea of the assembly will be the same regardless of processor, so you may as well start on the smaller instruction set and work your way up. By having the smaller instruction set you also open yourself up to having to face some interesting problems. For example, there is no multiply instruction on the 6502!
  • The instructions on the 6502 are varied and compared to other processors at the time offer a lot of options (in certain cases — we’ll discuss exactly what is meant here later).
  • The 6502 was incredibly popular. It was used in the Commodore 64, the NES, the Atari-2600, and more. Hundreds of millions of 6502 processors exist [3]. By learning 6502 assembly you are able to develop things for all these classic systems and understand their hardware.
  • I think its fun, but maybe I’m a nerd.

What Next?

When I wrote this originally it was meant to be for my office’s book-club where we each read a textbook. We went from the contents of this post to being able to write and compile a small program for the Apple II. Even getting to that point though there is a lot of ground to cover. We’ll need to learn about the instruction set, binary number, how to actually write assembly, and then how to compile and run it. This will all be split across another 4 posts and then we’ll see how it goes!

This is the first part of my “learning assembly” series:

This has been adapted from my personal blog

Most of the content I talk about will come from two main sources: “6502 Assembly Language Programming” by Lance A.Leventhal and “Programming the 6502” by Rodney Zaks.

--

--